﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="StyleCopRunner.cs" company="StyleCop for ReSharper Development Team">
//   Copyright (c) StyleCop for ReSharper Development Team. All rights reserved.
// </copyright>
// <summary>
//   Defines the StyleCopRunner type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace StyleCop.For.ReSharper
{
    #region Using Directives

    using System;
    using System.Collections.Generic;
    using System.IO;

    using Diagnostics;

    using JetBrains.DocumentModel;
    using JetBrains.ProjectModel;
    using JetBrains.ReSharper.Daemon;
    using JetBrains.Util;

    using Microsoft.StyleCop;
    using Microsoft.StyleCop.CSharp;

    using Options;

    using Violations;

    #endregion

    /// <summary>
    /// Executes Microsoft StyleCop within the Resharper Environment.
    /// </summary>
    /// <remarks>
    /// Microsoft StyleCop currently requires physical files for parsing. <see cref="StyleCopRunner"/>
    /// creates a shadow copy of the source file that is currently being parsed by ReSharper, 
    /// representing its current state during the editing process. This prevents the highlights from
    /// getting out of synch with the file in the IDE.
    /// </remarks>
    public class StyleCopRunner
    {
        #region Constants and Fields

        private const string CsParserId = "Microsoft.StyleCop.CSharp.CsParser";

        /// <summary>
        /// List of encountered violations, passed back to <see cref="StyleCopStageProcess"/> so that
        /// violations can be highlighted within the IDE.
        /// </summary>
        private readonly List<HighlightingInfo> violationHighlights = new List<HighlightingInfo>();

        /// <summary>
        /// Reference to the file currently being parsed by ReSharper.
        /// </summary>
        private IProjectFile file;

        /// <summary>
        /// Path to the shadow copy of the file currently being parsed by ReSharper.
        /// </summary>
        private List<string> filePath;

        private StyleCopCore styleCopCore;

        #endregion

        #region Properties

        /// <summary>
        /// A StyleCopCore instance.
        /// </summary>
        public StyleCopCore StyleCopCore
        {
            get
            {
                if (this.styleCopCore == null)
                {
                    this.styleCopCore = StyleCopReferenceHelper.GetStyleCopCore();

                    this.styleCopCore.Initialize(new List<string>(), true);
                    this.styleCopCore.DisplayUI = false;
                    this.styleCopCore.ViolationEncountered += this.OnViolationEncountered;
                }

                return this.styleCopCore;
            }
        }

        /// <summary>
        /// Gets List of encountered violations, passed back to <see cref="StyleCopStageProcess"/> so that
        /// violations can be highlighted within the IDE.
        /// </summary>
        /// <value>
        /// Gets a list of encountered violations.
        /// </value>
        public List<HighlightingInfo> ViolationHighlights
        {
            get
            {
                return this.violationHighlights;
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Executes <see cref="styleCopCore"/> within the <see cref="OnViolationEncountered"/>.
        /// </summary>
        /// <remarks>
        /// Violations are raised as events, handled by <see cref="IProjectFile"/>.
        /// </remarks>
        /// <param name="projectFile">
        /// <see cref="StyleCopStageProcess"/>representing the file currently being parsed by ReSharper.
        /// </param>
        /// <param name="document">
        /// The document being checked.
        /// </param>
        /// <param name="shadowFilePath">
        /// File Path to the Shadow Copy of the projectFile currently being parsed by ReSharper.
        /// </param>
        public void Execute(IProjectFile projectFile, IDocument document, List<string> shadowFilePath)
        {
            StyleCopTrace.In(projectFile, shadowFilePath);
            if (projectFile == null)
            {
                return;
            }
            
            if (!this.SkipAnalysisForDocument(projectFile))
            {
                var fileHeader = new FileHeader(Utils.GetCSharpFile(projectFile.GetSolution(), document));
                
                if (!fileHeader.UnStyled && StyleCopReferenceHelper.StyleCopIsAvailable())
                {
                    this.RunStyleCop(projectFile, shadowFilePath);
                }
            }

            StyleCopTrace.Out();
        }

        /// <summary>
        /// Gets the settings for the file provided.
        /// </summary>
        /// <param name="projectFile">
        /// The project file to get the Settings for.
        /// </param>
        /// <returns>
        /// Null if it couldn't find one.
        /// </returns>
        public Settings GetSettings(IProjectFile projectFile)
        {
            var settingsFile = this.FindSettingsFilePath(projectFile);

            if (string.IsNullOrEmpty(settingsFile))
            {
                var defaultSettingsPath = this.StyleCopCore.Environment.GetDefaultSettingsPath();
                return string.IsNullOrEmpty(defaultSettingsPath) ? null : this.StyleCopCore.Environment.GetSettings(defaultSettingsPath, true);
            }

            return this.StyleCopCore.Environment.GetSettings(settingsFile, true);
        }

        #endregion

        #region Methods

        /// <summary>
        /// Gets the settings file if it exists in this directory.
        /// </summary>
        /// <param name="directory">
        /// The directory.
        /// </param>
        /// <returns>
        /// The FileInfo for the settings file if one exists in this directory.
        /// </returns>
        private static FileInfo GetSettingsFileForDirectoryInfo(DirectoryInfo directory)
        {
            var settingsFileNames = new[] { "Settings.StyleCop", "Settings.SourceAnalysis" };

            foreach (var settingsFileName in settingsFileNames)
            {
                var foundSettingsFiles = directory.GetFiles(settingsFileName, SearchOption.TopDirectoryOnly);
                if (foundSettingsFiles.Length > 0)
                {
                    return foundSettingsFiles[0];
                }
            }

            return null;
        }

        /// <summary>
        /// Handles the ViolationEncountered Event, and 
        /// adds any violations to the <see cref="ViolationHighlights"/> Collection for display
        /// within the IDE.
        /// </summary>
        /// <param name="range">
        /// Text Range within the Document.
        /// </param>
        /// <param name="highlighting">
        /// Information to be highlighted.
        /// </param>
        private void CreateViolation(DocumentRange range, IHighlighting highlighting)
        {
            this.violationHighlights.Add(new HighlightingInfo(range, highlighting));
        }

        /// <summary>
        /// Searches this directory and the parents thereof to see 
        /// if a Settings file exists.
        /// </summary>
        /// <param name="directoryInfo">
        /// The directory.
        /// </param>
        /// <returns>
        /// The FileInfo for the settings file.
        /// </returns>
        private FileInfo FindSettingsFile(DirectoryInfo directoryInfo)
        {
            var settingsFilePath = GetSettingsFileForDirectoryInfo(directoryInfo);

            if (settingsFilePath == null && directoryInfo.Parent != null)
            {
                settingsFilePath = this.FindSettingsFile(directoryInfo.Parent);
            }

            return settingsFilePath;
        }

        /// <summary>
        /// Searches directorys of the project items project file and the parents thereof to see 
        /// if a Settings file exists.
        /// </summary>
        /// <param name="projectItem">
        /// File being examined.
        /// </param>
        /// <returns>
        /// Path to the settings file.
        /// </returns>
        private string FindSettingsFilePath(IProjectItem projectItem)
        {
            StyleCopTrace.In(projectItem);
            var projectFile = projectItem.GetProject();
            return StyleCopTrace.Out(this.FindSettingsFilePath(projectFile));
        }

        /// <summary>
        /// Searches directorys of the project file and the parents thereof to see 
        /// if a Settings file exists.
        /// </summary>
        /// <param name="project">
        /// The project file.
        /// </param>
        /// <returns>
        /// Path to the settings file.
        /// </returns>
        private string FindSettingsFilePath(IProject project)
        {
            if (project != null)
            {
                var directory = project.Location;

                if (directory.ExistsDirectory)
                {
                    var settingsFile = this.FindSettingsFile(directory.ToDirectoryInfo());

                    if (settingsFile != null)
                    {
                        return settingsFile.FullName;
                    }
                }
            }

            return null;
        }

        private PropertyValue GetParserSetting(IProjectFile projectFile, string propertyName)
        {
            StyleCopTrace.In(projectFile, propertyName);
            PropertyValue returnValue = null;

            var addIn = this.StyleCopCore.GetParser(CsParserId);

            if (addIn != null)
            {
                var settingsFile = this.FindSettingsFilePath(projectFile);
                if (!string.IsNullOrEmpty(settingsFile))
                {
                    var settings = this.StyleCopCore.Environment.GetSettings(settingsFile, true);

                    if (settings != null)
                    {
                        returnValue = addIn.GetSetting(settings, propertyName);
                    }
                }
            }

            return StyleCopTrace.Out(returnValue);
        }

        /// <summary>
        /// Returns an array of <see cref="CodeProject"/> which are required to fulfill the
        /// StyleCopCore.Analyze(IList{CodeProject}) method contract to parse the
        /// current file.
        /// </summary>
        /// <param name="core">
        /// StyleCopCore which performs the Source Analysis.
        /// </param>
        /// <returns>
        /// Returns an array of <see cref="CodeProject"/>.
        /// </returns>
        private CodeProject[] GetProjects(StyleCopCore core)
        {
            StyleCopTrace.In(core);
            var configuration = new Configuration(new[] { "DEBUG", "TRACE" });

            var codeProjects = new CodeProject[this.filePath.Count];
            var i = 0;

            foreach (var path in this.filePath)
            {
                var codeProject = new CodeProject(path.GetHashCode(), path, configuration);
                core.Environment.AddSourceCode(codeProject, path, null);
                codeProjects[i++] = codeProject;
            }

            return StyleCopTrace.Out(codeProjects);
        }

        private void LoadSettingsFiles(IList<CodeProject> projects, string settingsPath)
        {
            Settings mergedSettings = null;
            if (settingsPath != null)
            {
                var localSettings = this.StyleCopCore.Environment.GetSettings(settingsPath, false);
                if (localSettings != null)
                {
                    var merger = new SettingsMerger(localSettings, this.StyleCopCore.Environment);
                    mergedSettings = merger.MergedSettings;
                }
            }
            foreach (var project in projects)
            {
                var projectSettings = mergedSettings;
                if (projectSettings == null)
                {
                    projectSettings = this.StyleCopCore.Environment.GetProjectSettings(project, true);
                }

                if (projectSettings != null)
                {
                    project.Settings = projectSettings;
                    project.SettingsLoaded = true;
                }
            }
        }

        /// <summary>
        /// Called when the StyleCopCore.ViolationEncountered event is raised. Converts
        /// <see cref="ViolationEventArgs"/>into ReSharper Violation.
        /// </summary>
        /// <param name="sender">
        /// Object that raised the event.
        /// </param>
        /// <param name="e">
        /// Data Structure containing information about the Violation encoutered.
        /// </param>
        private void OnViolationEncountered(object sender, ViolationEventArgs e)
        {
            StyleCopTrace.In();
            var element = (CsElement)e.Violation.Element;

            if (element != null)
            {
                var a = e.SourceCode.Path;

                // if violations fire in the related files we ignore them
                if (a == this.filePath[0])
                {
                    var textRange = Utils.GetTextRange(this.file, e.LineNumber - 1);

                    // once we have a TextRange for the entire line reduce it to not include whitespace at the left or whitespace at the right
                    var documentRange = Utils.TrimWhitespaceFromDocumentRange(new DocumentRange(this.file, textRange));

                    var violation = StyleCopViolationFactory.GetHighlight(e, documentRange, element, e.LineNumber);
                    this.CreateViolation(documentRange, violation);
                }
            }

            StyleCopTrace.Out();
        }

        /// <summary>
        /// Runs StyleCop.
        /// </summary>
        /// <param name="projectFile">
        /// The project file.
        /// </param>
        /// <param name="shadowFilePath">
        /// The shadow file path.
        /// </param>
        private void RunStyleCop(IProjectFile projectFile, List<string> shadowFilePath)
        {
            StyleCopTrace.In(projectFile, shadowFilePath);

            try
            {
                this.file = projectFile;

                this.filePath = shadowFilePath;

                var projects = this.GetProjects(this.StyleCopCore);

                var settingsFile = this.FindSettingsFilePath(projectFile);
                StyleCopTrace.Info("In:  core.Analyze()");

                this.LoadSettingsFiles(projects, settingsFile);

                this.StyleCopCore.Analyze(projects);

                StyleCopTrace.Info("Out: core.Analyze()");
            }
            catch (Exception exception)
            {
                Logger.LogException(exception);
            }
            finally
            {
                StyleCopTrace.Out();
            }
        }

        private bool SkipAnalysisForDocument(IProjectFile projectFile)
        {
            if (!projectFile.Name.EndsWith(".cs"))
            {
                return true;
            }

            if (StyleCopOptions.Instance.UseExcludeFromStyleCopSetting && projectFile.ProjectFileIsExcludedFromStyleCop())
            {
                return true;
            }

            var analyzeDesignerFiles = true;
            var analyzeGeneratedFiles = false;

            var analyzeDesignerFilesSetting = this.GetParserSetting(projectFile, "AnalyzeDesignerFiles") as BooleanProperty;

            if (analyzeDesignerFilesSetting != null)
            {
                analyzeDesignerFiles = analyzeDesignerFilesSetting.Value;
            }

            if (analyzeDesignerFiles || !projectFile.Name.EndsWith(".Designer.cs", StringComparison.OrdinalIgnoreCase))
            {
                var analyzeGeneratedFilesSetting = this.GetParserSetting(projectFile, "AnalyzeGeneratedFiles") as BooleanProperty;

                if (analyzeGeneratedFilesSetting != null)
                {
                    analyzeGeneratedFiles = analyzeGeneratedFilesSetting.Value;
                }

                if (analyzeGeneratedFiles ||
                    (!projectFile.Name.EndsWith(".g.cs", StringComparison.OrdinalIgnoreCase) && !projectFile.Name.EndsWith(".generated.cs", StringComparison.OrdinalIgnoreCase)))
                {
                    return false;
                }
            }

            return true;
        }

        #endregion
    }
}